package edu.kit.csl.cratylus.ui;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import edu.kit.csl.cratylus.io.Logger;

/**
 * This singleton class stores the configuration, which is normally
 * set by command line arguments or configuration file.
 * 
 * @see #processCommandLineArguments(String[])
 */
public class Configuration {
	
	/*
	 * File name of the default configuration file
	 */
	final static private String DEFAULT_CONFIGURATION_FILE = "cratylus.conf";
	
	/*
	 * Singleton instance
	 */
	static private Configuration conf;
	
	/*
	 * Parameter lists by their datatypes
	 */
	private Map<String, Integer> intParams;
	private Map<String, Float> floatParams;
	private Map<String, String> stringParams;
	private Map<String, String> descriptions;
	
	/*
	 * List of already included configuration files 
	 */
	private Set<String> alreadyIncluded;
	
	/*
	 * Add an integer parameter.
	 */
	private void addParameter(String key, int defaultValue, String desc) {
		intParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add a floating point parameter.
	 */
	@SuppressWarnings("unused")
	private void addParameter(String key, float defaultValue, String desc) {
		floatParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add a string parameter.
	 */
	private void addParameter(String key, String defaultValue, String desc) {
		stringParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Private constructor.
	 */
	private Configuration() {
		descriptions = new HashMap<String, String>();
		intParams = new HashMap<String, Integer>();
		floatParams = new HashMap<String, Float>();
		stringParams = new HashMap<String, String>();
		alreadyIncluded = new HashSet<String>();
	}
	
	/**
	 * Restores the default configuration.
	 */
	public void restore() {
		descriptions.clear();
		intParams.clear();
		floatParams.clear();
		stringParams.clear();
		SimpleDateFormat formatter = new SimpleDateFormat("yy-MM-dd.HHmmss.");
		addParameter("alignmentFile", "",
				"REQUIRED! Path to the alignment file.");
		addParameter("alignmentLoader", "pisa",
				"Loader for the alignment file. Only 'pisa' is implemented, "
				+ "which loads the alignments in PISA format.");
		addParameter("configFile", "",
				"Configuration file (GIZA++ format: <key> <val>). Parameters "
				+ "in this file override values in the default configuration "
				+ "file. Command line arguments override all other settings. "
				+ "If this parameter is used within a config file, it has "
				+ "'include once' semantics.");
		addParameter("logLevel", "INFO",
				"Controls the amount of output. "
				+ "FATAL: Only fatal errors, "
				+ "ERROR: All errors, "
				+ "WARN: Warnings and errors, "
				+ "INFO: Notices, warnings and errors, "
				+ "DEBUG: Debug mode");
		addParameter( // Like GIZA++
				"outputPrefix",
				formatter.format(new Date()) + System.getProperty("user.name"),
				"Prefix which is prepended to all output files");
		addParameter("outputFormat", 3,
				"This parameter controls the output format of the corpus. You "
				+ "can produce multiple outputs by adding their values.\n"
				+ "1: Plain: Output two plain files, in which corresponding "
				+ "sentence pairs are in the same line. Suitable for passing-"
					+ "-through to Moses.\n"
				+ "2: GIZA++: Output the alignments in GIZA++ format with "
					+ "word labels.\n"
				+ "4: PISA: Output the alignments as word-to-phoneme "
					+ "alignments with word boundaries.");
		addParameter("writingSystem", "p2g",
				"The writing system defines the way the phonemes in the target"
				+ " language are translated to a target language word.\n"
				+ "'p2g': Use a mapping file p2gFile for the mapping between "
				+ "target language phonemes and graphemes.\n"
				+ "'dropBlanks': The target words are represented by the "
				+ "original phonemes without blanks.\n"
				+ "'source': The target words are represented by the aligned "
				+ "source word. This may conflict with splitters other than "
				+ "marx since different target words may have the same "
				+ "written representation.");
		addParameter("p2gFile", "p2g.conf",
				"This parameter is only significant if the writing system "
				+ "'p2g' is used. The file format is the same as in GIZA++");
		addParameter("outputVocabulary", 1,
				"This parameter defines the format of the pronunciation "
				+ "dictionary. You can produce multiple outputs by adding "
				+ "their values or supress it completely by setting to 0.\n"
				+ "1: Janus: Output the pronunciation dictionary in Janus "
					+ "format.");
		addParameter("planner", "sourceWordFrequency",
				"The planner decides in which order target words are "
				+ "proceeded. The only implemented way is sourceWordFrequency"
				+ " to order the source words by frequency, and give the set "
				+ "of aligned target words to the splitter.");
		addParameter("splitter", "elki",
				"Strategy for splitting the candidate sets of the planner into"
				+ " smaller pieces. Options are:\n"
				+ "elki: Use the clustering library ELKI for a variety of "
					+ "different clustering algorithms. See the ELKI "
					+ "documentation for detailed information about available"
					+ "algorithms and parameters. These settings can be passed"
					+ "-through to ELKI via the parameters splitterAlgorithm "
					+ "and splitterArguments.\n"
				+ "marx: Do not split at all.\n"
				+ "sartre: Treat each candidate separately (the merger is not "
					+ "relevant anymore when this option is active)");
		addParameter("splitterAlgorithm", "DBSCAN",
				"This option only applies when the splitter elki is used. It "
				+ "defines the clustering algorithm to use. Consult the ELKI "
				+ "documentation for more information to the algorithms, in "
				+ "particular to the splitterParam* parameters. Options are:\n"
					+ "DBSCAN, DeLiClu, OPTICS, SLINK");
		addParameter("splitterDistanceFunction", "levenshtein",
				"Distance function used by the splitter. Options are:\n"
				+ "levenshtein: Use the Levenshtein distance as metric.\n"
				+ "relativeLevenshtein: Use the Levenshtein distance divided "
					+ "by the length.");
		addParameter("splitterParamMinPts", 3,
				"minPts parameter of the splitterAlgorithm");
		addParameter("splitterParamEpsilon", 1,
				"epsilon parameter of the splitterAlgorithm");
		addParameter("merger", "frequency",
				"The merger finds one representative for a set of candidates."
				+ " Possible values are:\n"
				+ "random: Randomly pick some candidate in the set.\n"
				+ "frequency: Pick the most frequent candidate in the set.\n"
				+ "srilm: Run nbest-lattice from the SRI language modeling "
					+ "toolkit. Note the mergerToolPath and "
					+ "mergerToolArgs parameter.\n"
				+ "msa: Use Multi Sequence Alignment (Profile HMMs) to merge "
					+ "all candidates. Note the mergerToolPath and "
					+ "mergerToolArgs parameter. NOT implemented yet!");
		addParameter("mergerToolPath", "",
				"If the merger requires a tool, this parameter has to point"
				+ " to a executable binary of it.");
		addParameter("mergerToolArgs", "",
				"This parameter contains command line arguments passed through"
				+ " to the merger tool (if used).");
		addParameter("maxWordExtension", 2,
				"This parameter is only used when the corpus updater align is "
				+ "activated. It is the maximum number of phonemes a adjacent "
				+ "word can be extented. This parameter avoids assigning whole"
				+ " words to the neighbor.");
		addParameter("corpusUpdater", "replace",
				"The corpus updater writes back the word labels to the "
				+ "Alignments. Possible values are:\n"
				+ "replace: Replace old target word with new representative"
				+ "align: Align the representative to the phoneme sequence and"
					+ "update adjacent target words if necessary.");
	}
	
	/**
	 * Get configuration singleton. If it does not exist yet, create it and 
	 * load default configuration file.
	 * 
	 * @return configuration singleton
	 */
	static public Configuration getSingleton() {
		if (conf == null) {
			conf = new Configuration();
			conf.restore();
			conf.loadConfigurationFile(DEFAULT_CONFIGURATION_FILE);
		}
		return conf;
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, int value) {
		if (!intParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown integer parameter " + key);
		} else {
			intParams.put(key, value);
		}
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, float value) {
		if (!floatParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown floating point parameter " + key);
		} else {
			floatParams.put(key, value);
		}
	}
	
	/**
	 * Get a integer parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Integer getInteger(String key) {
		return intParams.get(key);
	}
	
	/**
	 * Get a floating point parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Float getFloat(String key) {
		return floatParams.get(key);
	}
	
	/**
	 * Get a string parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public String getString(String key) {
		return stringParams.get(key);
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, String value) {
		try {
			if (intParams.containsKey(key)) {
				set(key, Integer.parseInt(value));
			} else  if (floatParams.containsKey(key)){
				set(key, new Float(value));
			} else if (stringParams.containsKey(key)) {
				stringParams.put(key, value);
				if (key.equals("logLevel")) {
					Logger.getSingleton().setLevel(value);
				}
			} else {
				Logger.getSingleton().fatalError(
						"Unknown parameter " + key);
			}
		} catch(NumberFormatException e) {
			Logger.getSingleton().fatalError(
					"Parameter " + key + " should be numeric");
		}
	}
	
	/**
	 * Parse the command line arguments and overrides parameters if they
	 * appear in them.
	 * 
	 * @param args string array with command line arguments
	 */
	public void processCommandLineArguments(String[] args) {
		String curParam = null;
		for (String arg : args) {
			if (arg.charAt(0) == '-') {
				if (curParam != null) {
					set(curParam, 1);
				}
				curParam = arg.substring(1);
			} else if (curParam != null) {
				set(curParam, arg);
				if (curParam.equals("configFile") 
						&& loadConfigurationFile(arg)) {
					// Process command line arguments again
					processCommandLineArguments(args);
					return;
				}
				curParam = null;
			} else {
				Logger.getSingleton().fatalError(
						"Malformed command line argument " + arg);
			}
		}
	}
	
	/**
	 * Load parameters from a configuration file. The file should be in GIZA++
	 * format (i.e. one parameter per line, key/value separated by a blank).
	 * Lines starting with '#' are ignored.
	 * 
	 * @param fileName path to the file to load
	 * @return <code>true</code> if the file could be loaded, false otherwise
	 */
	public boolean loadConfigurationFile(String fileName) {
		if (alreadyIncluded.contains(fileName)) {
			return false;
		}
		Logger log = Logger.getSingleton();
		alreadyIncluded.add(fileName);
		try {
			FileInputStream fstream = new FileInputStream(fileName);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				if (strLine.charAt(0) == '#') { // Comment
					continue;
				}
				String[] split = strLine.split(" ", 2);
				if (split.length != 2) {
					log.error("Malformed line " + strLine + " in " + fileName);
				}
				set(split[0], split[1]);
			}
			in.close();
		} catch (Exception e) {// Catch exception if any
			log.error("Error while reading config file "
					+ fileName + ": " + e.getMessage());
			return false;
		}
		return true;
	}
	
	/**
	 * Prints a help text to stdout.
	 */
	public void printHelp() {
		System.out.println("Cratylus is a tool for extracting training data"
				+ " for ASR and MT systems from cross-lingual word-to-"
				+ "phoneme alignments found by PISA.");
		System.out.println(
				"Configuration via command line arguments: -<name> <value>");
		System.out.println(
				"Configuration via file (per line): <name> <value>");
		System.out.println("Following parameters are available.");
		System.out.println("");
		System.out.println("String parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, String> entry : stringParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue(),
					descriptions.get(entry.getKey()));
		}
		System.out.println("");
		System.out.println("Integer parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, Integer> entry : intParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue().toString(),
					descriptions.get(entry.getKey()));
		}
		System.out.println("");
		System.out.println("Floating point parameters");
		System.out.println("---------------------");
		for (Map.Entry<String, Float> entry : floatParams.entrySet()) {
			printParameter(entry.getKey(), entry.getValue().toString(),
					descriptions.get(entry.getKey()));
		}
	}
	
	/*
	 * Prints a single parameter to stdout.
	 */
	private void printParameter(String key, String value, String description) {
		if (value.equals("")) {
			value = "<not set>";
		}
		System.out.println(key + " (Default: " + value + ")");
		System.out.println(description);
		System.out.println("");
	}
}
